# -*- coding: utf-8 -*-
import os # per os.join
import math
 
# import panda main module
import direct.directbase.DirectStart

from pandac.PandaModules import deg2Rad, rad2Deg
from direct.showbase.PythonUtil import rad90, rad180

from direct.gui.OnscreenText import OnscreenText
 
# questi sono anche su main.py (...)
K_POS = 'position'
K_HPR = 'orientation'
 
WORKDIR = 'models' #questo forse dovrebbe stare a livello di main.py
 
class Unit(object): #per ora eredita da "object" (v. classi new style)
    
    # speed has to be specified in km/h or m/s
    # now it's in m/frame
    cruiseSpeed = 0.06
    # angular velocity in deg/frame
    angleSpeed = 0.50
    angleTolerance = 0.5
    textObject = OnscreenText(text='In attesa di movimento', pos=(0.0, -0.5), scale=0.07, fg=(1, 0, 0, 1))
    oldx, oldy, oldz = 0, 0, 0
    
    def __init__(self, name='unit', map=None, position=None, orientation=None):
        # NB: name dovrebbe essere uguale al nome del modello
        # NB: 'name' should be the name of the model file
        self.name = name
        self.map = map
        self.speed = 0 # scalar of velocity
        self.hRad = 0
        if name == 'unit':
            self.create_tank()
        else:
            self.modelPath = os.path.join(WORKDIR, name)
            # NodePath (per ora uso i models, poi si useranno gli actors)
            # Actors will be likely used in future.
            self.nodePath = loader.loadModel(self.modelPath)
            self.nodePath.setScale(1, 1, 1) # questo dipenderà dal modello...
        self.nodePath.reparentTo(render)
        print self.nodePath.getBounds() # forse servirà per capire le dimensioni e le collisioni
        if position:
            x, y, z = position
            self.set_pos(x, y, z)
        if orientation:
            h, p, r = orientation
            self.set_orientation(h, p, r)
    
    def __repr__(self):
        return self.name
    
    def __setattr__(self, name, value):
        '''Set some nodepath values.
        NB: self.x = 15, self.pos = (1, 2, 3), and so on, have an actual visual effect
        '''
        if name == 'x':
            self.nodePath.setX(value)
        elif name == 'y':
            self.nodePath.setY(value)
        elif name == 'z':
            self.nodePath.setZ(value)
        elif name == 'pos':
            x, y, z = value
            self.x, self.y, self.z = x, y, z
        elif name == 'h':
            self.nodePath.setH(value)
            self.hRad = deg2Rad(value)
        elif name == 'p':
            self.nodePath.setP(value)
        else:
            object.__setattr__(self, name, value)
        
    def set_pos(self, x=None, y=None, z=None):
        "Sets unit's nodepath pos."
        if x is not None:
            self.x = x
        if y is not None:
            self.y = y
        if z is not None:
            self.z = z
    
    @property
    def x(self):
        return self.nodePath.getX()
    
    @property
    def y(self):
        return self.nodePath.getY()
    
    @property
    def z(self):
        return self.nodePath.getZ()
    
    @property
    def h(self):
        return self.nodePath.getH()
    
    def set_orientation(self, h, p, r):
        self.nodePath.setHpr(h, p, r)
    
    def set_speed(self, scalar):
        'Sets the scalar value for velocity, also updating vx and vy components'
        self.speed = scalar
        self.vx = self.speed * -math.sin(self.headToDestRad) # prestation issue (recalculates vx also if self.headRad does not change)
        self.vy = self.speed * math.cos(self.headToDestRad) # prestation issue (recalculates vy also if self.headRad does not change)

    def update_pos(self):
        "Updates the unit's nodepath position from its velocity"
        self.x += self.vx
        self.y += self.vy
        self.z = self.map.elevation(self.x, self.y)
        x, y, z = self.x, self.y, self.z
        h_distance = math.sqrt((x - self.oldx)**2 + (y - self.oldy)**2)
        v_distance = self.oldz - z
        self.oldx, self.oldy, self.oldz = x, y, z
        # adjust pitch (experimental)
        # p = - (v_distance/h_distance) * 90.0
        # uses the terrain normal in order to make the unit keep parallel to the terrain
        # issue13: the normal doesn't correspond to the terrain slope
        pix_x, pix_y = self.map.xy_to_pixel(x, y)
        normal = self.map.map_terrain.getNormal(pix_x, pix_y)
        nx, ny, nz = normal
        p = (90 - 90 * nz)
        self.p = p
        # cancel previous text and write the new one
        self.textObject.destroy()
        self.textObject = OnscreenText(text='pos=(%(x).1f m, %(y).1f m, %(z).1f m) [%(pix_x)ipx,%(pix_y)ipy] - normal = %(nx)f-%(ny)f-%(nz)f - pitch = %(p).1f' % locals(),
            pos=(-0.1, -0.8), scale=0.07, fg=(1, 1, 1, 1))
    
    def goto(self, x, y):
        'Sets a destination with coordinates and move'
        print "%s: I'm going to %s, %s" % (self.name, x, y)
        self.set_destination(x, y)
        # shortest turning direction
        turn = shorter_way_in_circle(self.h, self.headToDest)
        print 'turning', {1:'left', -1:'right', 0:None}[turn]
        # turn and move
        taskMgr.doMethodLater(2, self.heading, 'heading', extraArgs=[turn], appendTask=True)
    
    def stop(self):
        print 'Stopping'
        self.set_speed(0)

    def set_destination(self, x, y):
        self.destination = x, y
        print 'Destination =', self.destination
        print 'Position =', self.x, self.y
        dx, dy = x - self.x, y - self.y
        # distance to destination
        distance = math.sqrt(dx**2 + dy**2)
        print 'Distance =', distance
        
        # angle between r and +Y (assume r = (dest - pos) vector)
        hdRad = calc_head(self.x, self.y, x, y, debug=True)
        
        self.headToDestRad = hdRad
        # conversion to deg
        destHead = rad2Deg(hdRad)
        self.headToDest = destHead
        print 'My head =', self.h
        print 'Head to position =', destHead
        print 'Head to position (rad) = %.2fπ' % (hdRad/3.141592)
        print 'Delta =', (destHead - self.h)

    def calc_dest_distance(self):
        'Calculates the distance between the unit and the setted destination'
        dstX, dstY = self.destination
        delta = math.sqrt((self.x - dstX)**2 + (self.y - dstY)**2)
        return delta
    
    def destination_reached(self, tolerance=0.5):
        'Check if destination is reached'
        delta = self.calc_dest_distance()
        return delta < tolerance
    
    def heading(self, turn, task):
        'Turns towards destination and move to it'
        # turning step
        self.h += turn * self.angleSpeed
        # calculate the remaining angle
        deltAngleAbs = abs(self.h - self.headToDest)
        if not task.frame % 50: # every 50 frames print angles
            print self.h, deltAngleAbs #debug
        if deltAngleAbs <= self.angleTolerance:
            # Actual unit's head is almost equal to the right direction for destination
            self.h = self.headToDest
            print 'Headed to', self.h
            self.set_speed(self.cruiseSpeed)
            taskMgr.doMethodLater(1, self.moving, 'moving')
            print 'Moving'
            return task.done
        else:
            return task.cont

    def moving(self, task):
        'Just moves the unit until its destination is reached'
        self.update_pos()
        if self.destination_reached():
            print 'Destination reached'
            self.stop()
            return task.done
        else:
            # sposta fisicamente
            self.update_pos()
            return task.cont

    def create_tank(self):
        'Creates a generic tank.'
        mainSize = 1
        sx, sy, sz = 3, 5, 1
        msx, msy, msz = mainSize * sx, mainSize * sy, mainSize * sz
        self.nodePath = loader.loadModel('box')
        self.nodePath.setColor(0.1, 1, 0.5)
        self.nodePath.setScale(msx, msy, msz)
        cupola = loader.loadModel('box')
        cupola.reparentTo(self.nodePath)
        cupola.setScale(0.5, 0.5, 0.5)
        cupola.setPos(0.25, 0.15, 1)
        cannon = loader.loadModel('box')
        cannon.reparentTo(cupola)
        cannon.setScale(0.1, 2, 0.3)
        cannon.setPos(0.45, 1, 0.3)
        # NB ho fatto tutto a mano per tentativi perché ancora non ho ben chiaro
        # come funzionano le coordinate e le scalature relative ai nodi figli

# questa sarebbe chiamata da main.py
def load(main, dic):
    units = []
    for name in dic:
        unitData = dic[name]
        position = unitData[K_POS]
        x, y, z = position
        if K_HPR in unitData:
            orientation = dic[name][K_HPR]
        else:
            orientation = 0, 0, 0
        h, p, r = orientation
        unit = Unit(name)
        unit.set_pos(x, y, z)
        unit.set_orientation(h, p, r)
        units.append(unit)
    return units

def shorter_way_in_circle(a, b, u='deg'):
    '''
    Given 2 deg angles it returns the sign that represents the shorter way direction to go from a to b
    '''
    if u == 'deg':
        flatAngle = 180
    elif u == 'rad':
        flatAngle = rad180
    if a == b:
        res = 0
    elif b > a:
        if b - a <= flatAngle:
            res = 1
        else:
            res = -1
    else:
        # a > b
        if a - b <= flatAngle:
            res = -1
        else:
            res = 1
    return res
    

def calc_head(x1, y1, x2, y2, debug=False):
    'Returns a radiant head angle pointing from (x1, y1) to (x2, y2)'
    dx = x2 - x1
    dy = y2 - y1
    r = math.sqrt(dx**2 + dy**2)
    # thrad = to-targed head in radiants
    if dy >= 0 and dx >= 0:
        direction = 'NE'
        # to NE
        # dx = r * cos (rad90 - thrad) = r * sin (thrad)
        thrad = math.asin(dx/r)
    elif dy >= 0 and dx <= 0:
        direction = 'NW'
        # to NW
        thrad = 2 * math.pi + math.asin(dx/r)
    elif dy <= 0 and dx <= 0:
        direction = 'SW'
        # to SW
        thrad = - 1.5 * math.pi + math.acos(dx/r)
    elif dy <= 0 and dx >= 0:
        direction = 'SE'
        # to SE
        thrad = rad180 + math.asin(- dx/r)
    if thrad > rad180:
        thrad = - (2 * math.pi - thrad)
    if thrad < - rad180:
        thrad = - (2 * math.pi + thrad)
    if debug:
        print 'heading to', direction
    return -thrad
